按代码执行顺序讲: ① 权重加载 → ② 主干推理(1次)→ ③ 草稿循环(只跑1层)→ ④ 验证推理(再跑1次主干)→ ⑤ 接受/拒绝
# 加载权重(这就是全部)
trunk = weights[0:64] # 主干 64层(标准 Decoder Block)
mtp = weights[64] # MTP Head 1层(轻量 Decoder Block)
lm = weights.lm_head # 共享 LM Head(词表映射)
# 后续所有流程都复用这三块,草稿和验证共用 trunk 和 lm。
trunk 和 lm,唯一多出来的是 mtp 这 1 层。
num_hidden_layers=64 ✓、
mtp_num_hidden_layers=1 ✓(MTP 确为 1 层)、
mtp_use_dedicated_embeddings=false ✓(MTP 直接复用主模型共享 token 嵌入,与"取前一个 token 的 embedding 拼接"一致)、
tie_word_embeddings=false(LM Head 为独立矩阵,被草稿 / 验证两条路径共用)。
结论:本文"64 层主干 + 1 层 MTP Head + 共享 LM Head"的三组件拆解与真实模型权重结构一致。
# 主干推理(仅此一次)
def trunk_forward(context):
h = embed(context) # token -> embedding
for layer in trunk: # 循环 64 层
h = layer(h)
return h # 隐藏状态 h (seq, 4096)
# 在一个生成步中,只调用一次
h = trunk_forward(context) # ← 这就是唯一的“重”计算
cache_h = h # 缓存起来,后续草稿用
h 被缓存后,草稿阶段完全不再调用 `trunk`,只复用这个 h。
mtp 和 lm,代价是 1 层 vs 64 层。# 草稿推理(循环 N 次,默认 2~3)
def draft_loop(h, prev_token, num_drafts=2):
drafts = []
prev_emb = embed(prev_token) # 上一 token 的 embedding
for _ in range(num_drafts):
# ★ 关键:这里只有 mtp(1层)和 lm,没有 trunk!
h_mtp = forward(mtp, concat(h, prev_emb)) # 只过 1 层
logits = lm(h_mtp) # 过共享 LM_Head
token = sample(logits) # 采样
drafts.append(token)
prev_emb = embed(token) # 下一轮用刚猜出的 token
h = h_mtp # 下一轮用 MTP 的输出
return drafts
# 使用缓存的 h 和最后一个真实 token
drafts = draft_loop(h, last_real_token, num_drafts=2)
# 得到 [草稿1, 草稿2]
mtp(1 层)和 lm。这是草稿极快的根本原因。
# 验证推理(再跑一次主干)
def verify(context_with_drafts):
# 第二次调用 trunk(带 KV Cache,只算新位置)
h_full = forward(trunk, context_with_drafts) # 上下文+草稿一起过
logits = lm(h_full) # 每个位置的真实分布
return logits
# 拼装完整序列
full_context = context + drafts
verify_logits = verify(full_context)
# 得到 [位置1的真实分布, 位置2的真实分布, ...]
trunk(64层),但 KV Cache 让“上下文”部分完全跳过,只计算新增的草稿 token 位置,所以实际新增计算量远小于 64 层。
def accept_reject(drafts, verify_logits):
accepted = []
for i, draft_token in enumerate(drafts):
real_token = argmax(verify_logits[i]) # 真实分布的最大概率 token
if draft_token == real_token:
accepted.append(draft_token) # 猜对了,收下
else:
accepted.append(real_token) # 猜错了,用真实的替换
break # 遇到第一个错误就停
return accepted
# 最终输出
final_tokens = accept_reject(drafts, verify_logits)
unsloth/Qwen3.8-27B-GGUF / Qwen3.8-27B-UD-Q4_K_M.gguf(16.46GB);config.json(Qwen/Qwen3.8-27B)核对架构超参;gguf.GGUFReader 解析张量名 / 形状 / 元数据,逐项比对。
📊 关键物证(来自真实 GGUF)
| 真实权重事实 | 对应本文说法 | 结论 |
|---|---|---|
block_count = 65blk.0~63 主干 + blk.64 为 MTP |
64 层主干 + 1 层 MTP Head | ✅ |
nextn_predict_layers = 1full_attention_interval = 4 |
MTP 只 1 层;主干为混合注意力 | ✅ |
顶层仅一个 output.weight [5120, 248320](共享 LM Head)一个 token_embd.weight [5120, 248320] |
验证 / 草稿两条路径共用同一 lm | ✅ |
blk.64.nextn.eh_proj.weightshape = [10240, 5120] |
取 h(5120) 拼接前一个 token 的 embedding(5120),再过一层 fc → 5120 | ✅ |
blk.64 含完整 attn/ffn/norm(15 张量)+ 4 个 nextn 专属张量,其中 shared_head_norm |
MTP 输出经 shared_head_norm 复用同一 LM Head | ✅ |
qwen35(Qwen3.5 系),社区 / GGUF 仓名为 "Qwen3.8",属命名差异,不影响机制结论。校验脚本见 verify_qwen38_mtp.py,验证归档见 tests/result/。